Написание плагинов для Kruptar 7
Автор: Griever

Итак, сегодня мы будем расширять функциональность Круптара. Многим уже полюбился этот чрезвычайно гибкий инструмент для выдирания, редактирования и вставки обратно игрового скрипта. И сегодня, дорогой друг, ты поймёшь, что его возможности действительно безграничны, ибо он позволяет редактировать и упакованный текст! Конечно, для этого потребуется чуть больше, чем базовые знания в ромхакинге, но ведь когда-то нужно развиваться. Но обо всём по порядку.

Для хорошего усвоения нового урока нам понадобятся:

7-й Круптар

Исходники плагинов под него (нас будет интересовать Null плагин)

Delphi (у меня была 7-я версия) и, разумеется, знание языка.

РОМ-жертва: Snake's Revenge (U).nes

Вскрытие пациента.

Для начала нужно поближе познакомиться с нашим сегодняшним гостем: Snake's Revenge. Про игру много говорить не буду (многие её не жалуют), зато в желающих перевести её дефицита нет (насколько я знаю, игра до сих пор полностью не переведена). На самом деле, основной скрипт здесь не зажат, зато текст, появляющийся в TRCVR (прадедушка Codec’а), не хочет отыскиваться Relative Search’ем. Поэтому после непродолжительных манипуляций (содержание которых выходит за рамки данного документа) находим, наконец, вот такой кусок кода, отвечающий за распаковку текста в RAM, откуда в дальнейшем он перемещается в память PPU (кто привык копаться в голых данных эмпирическими методам и методами научного тыка могут сразу переходить к концу данной главы):

 

ROM:A75A uncomp:                                 ; CODE XREF: ROM:A7BEj

ROM:A75A                 LDA     byte_41        ;загружаем номер сообщения

ROM:A75C                 ASL     A               ;умножаем на два (указатели двухбайтовые)

ROM:A75D                 TAY

ROM:A75E                 LDA     PTR_table,Y

ROM:A761                 STA     Ptr_low

ROM:A763                 LDA     PTR_table+1,Y 

ROM:A766                 STA     Ptr_high         ;Загружаем соответствующий указатель

ROM:A768                 LDA     byte_42         ;Загружаем смещение от начала сообщения

ROM:A76A                 BNE     loc_A76F        ;В начале здесь всегда ноль.

ROM:A76C                 STA     Offset

ROM:A76F

ROM:A76F loc_A76F:                               ; CODE XREF: ROM:A76Aj

ROM:A76F                 LDY     Offset

ROM:A772                 LDX     #0

ROM:A774                 LDA     (Ptr_low),Y     ;Загружаем байт из упакованного потока

ROM:A776

ROM:A776 loc_A776:                               ; CODE XREF: ROM:loc_A7B1j

ROM:A776                 LSR     A

ROM:A777                 LSR     A

ROM:A778                 STA     Unpacked_char_Buffer,X; сохранение в поток распакованного

ROM:A77B                 INX                                 ; текста

ROM:A77C                 CPX     #$40 ; '@'     ;в сообщении не больше $40 символов

ROM:A77E                 BEQ     loc_A784

ROM:A780                 CMP     #$3F ; '?'      ; $3F- конец распаковки

ROM:A782                 BNE     Index_Operation

ROM:A784

ROM:A784 loc_A784:                               ; CODE XREF: ROM:A77Ej

ROM:A784                 STY     Offset

ROM:A787                 JMP     loc_A7C0

ROM:A78A ; ---------------------------------------------------------------------------

ROM:A78A

ROM:A78A Index_Operation:                        ; CODE XREF: ROM:A782j

ROM:A78A                 TXA

ROM:A78B                 LSR     A     ; Хитрая процедура, чтобы в случае, если из

ROM:A78B                                ; предыдущего байта в последующий перекидывается 6

ROM:A78B                                ; бит, да еще от двух младших избавляемя, чтобы этот

ROM:A78B                                ; последующий байт не пропал - в итоге при X=3

ROM:A78B                                ; Y не изменится

ROM:A78C                 LSR     A

ROM:A78D                 STA     byte_7

ROM:A78F                 TXA

ROM:A790                 CLC

ROM:A791                 SBC     byte_7

ROM:A793                 CLC

ROM:A794                 ADC     Offset

ROM:A797                 TAY

ROM:A798                 TXA

ROM:A799                 AND     #3

ROM:A79B                 STA     Counter

ROM:A79D                 LDA     (Ptr_low),Y  ;Загружаем байт из упакованного потока

ROM:A79F                 STA     First_Byte

ROM:A7A1                 INY

ROM:A7A2                 LDA     (Ptr_low),Y  ;Загружаем байт из упакованного потока

ROM:A7A4

ROM:A7A4 TwoBits_InSecondByte:                   ; CODE XREF: ROM:A7AEj

ROM:A7A4                 DEC     Counter

ROM:A7A6                 BMI     loc_A7B1

ROM:A7A8                 LSR     First_Byte     ; Два младших бита первого байта переходят

ROM:A7A8                                          ; в два старших второго, причем два младших

ROM:A7A8                                          ; бита второго уже не используются

ROM:A7AA                 ROR     A

ROM:A7AB                 LSR     First_Byte

ROM:A7AD                 ROR     A

ROM:A7AE                 JMP     TwoBits_InSecondByte

ROM:A7B1 ; ---------------------------------------------------------------------------

ROM:A7B1

ROM:A7B1 loc_A7B1:                               ; CODE XREF: ROM:A7A6j

ROM:A7B1                 JMP     loc_A776 ; дальнейшая обработка распакованного текста

 

 

Испугался? Думаю, в графическом представлении все выглядит не так страшно:

 

Если кто ещё не догадался, то это банальная шестибитная кодировка одного символа. То есть в индексе каждого символа используется не 8 бит (целый байт), а всего 6 (два старших не используются). Легко подсчитать, что двоичное 00111111 – даёт нам максимально возможных 64 используемых символа (а нам больше и не надо). Таким образом, имеем входной битовый поток, в котором, например, в первом байте будут 6 бит первого символа и ещё два бита от второго символа, во втором байте – уже четыре бита второго символа и четыре бита третьего символа и так далее. Разумеется, relative search не мог обнаружить текст, упакованный таким образом.

 

По старинке.

Ну, как водится, можно было бы написать простенькую программку, которая бы читала входной поток и выдавала в выходной индексы символов (практически готовый скрипт, только его ещё надо прогнать через таблицу). Для подтверждения эмпирических данных, полученных из анализа кода игры. Так и сделаем:

 

Sinput:=Tmemorystream.Create;    //Входной файл

Soutput := Tmemorystream.Create; //Выходной файл

Sinput.Position := Offset;           //Оffset – смещение начала упакованного сообщения

Sinput.LoadFromFile(Filename);

 

Bit := 0;//Временная переменная, которая будет носить в себе один бит

SI := 0;//Счетчик бит входного потока

DI := 0;//Счетчик бит выходного потока

DB := 0;// Destination_Byte - Байт выходного потока

count := 0;// Счетчик сколько нам нужно распаковать байт

 

Sinput.Read(SB,1);// Source_Byte - SB – байт входного потока

 

While Count < $30 do //для контроля распакуем 48 символов

Begin

 

 Bit := (SB and $80) shr 7;//берём старший бит входного байта

 SB := SB shl 1;

 inc (SI);

 If SI = 8 Then

 Begin               //Если байт входного потока исчерпан, читаем новый

  SI :=0;

  SInput.Read(SB,1);

 end;

 

 DB := DB shl 1;

 DB:= DB or Bit;  //Записываем наш бит в младший бит выходного потока

 inc (DI);

 If DI = 6 then

 Begin            //Если байт выходного потока исчерпан, сохраняем его в выходной поток.

  DI:=0;          

  SOutput.Write(DB,1);

  DB :=0;

  inc (Count);

 end;

end;

SOutput.SaveToFile('Out.bin');

Sinput.free;

Soutput.Free;

 

 

Думаю, излишне говорить, что распакованный файл по своему содержимому должен совпадать с текстовой частью тайловой карты, когда в неё выводится игровой текст, которую можно увидеть в Name Table Viewer в FCEUXD, плюс байты конца строки и конца сообщения (вот заодно и узнаем их значения и сможем окончательно составить таблицу).

Кстати о значениях байтов: что ещё мы можем узнать из кода игры? Ну, начнём со стандартной процедуры: составление таблицы (мою можно найти в проекте Круптара, распаковав его как .zip архив). Обратили внимание на строки:

3B=

3C="

Откуда я узнал? Да вот отсюда:

ROM:A7C5                 LDA     Unpacked_char_Buffer,Y

ROM:A7C8                 CMP     #$3B ; ';'

ROM:A7CA                 BEQ     loc_A7FD ; процедура подмены индекса на пробел

ROM:A7CC                 CMP     #$3C ; '<'

ROM:A7CE                 BNE     loc_A7FF

ROM:A7D0                 LDA     #$75 ; в Pattern Table это индекс значка "

ROM:A7D2                 BNE     loc_A7FF

Строки типа:

3D

3E

ends

3F

специфичны для Круптара и означают, что $3D и $3E – байты разрыва строки, а $3F – байт конца строки.

И, наконец, финальный аккорд: когда я осматривал таблицу указателей (она есть в коде – я её переименовал в PTR_table), то обнаружил, что она расположена прямо перед блоками с текстом, но между старшим байтом последнего указателя и местом, на которое указывает первый указатель, стоит один непонятный байт! Можете убедиться сами, открыв РОМ по смещению 0хEA88 – перед ним стоит указатель на последний текстовый блок (имеет значение $B0A3, затем идёт неизвестный байт $03, затем (по смещению 0xEA89) первый байт упакованного потока текста, на который ссылается первый указатель таблицы со значением $AA79). Экспериментально распаковав несколько байт второго текстового блока своей тестовой программкой, и дойдя до этого текста в игре (это брифинг пилота о том, что нужно проникнуть в логово врага и собрать информацию – сразу после прохождения первого экрана), поменяем этот байт, скажем, на ноль, единичку и т.п. и выясним, что этот байт отвечает за то, кто именно говорит в этот момент по TRCVR (волосатый мужик, негр, девушка, пилот…). Назовём его байтом атрибута текста. С ним придётся считаться: скорее всего, он расположен перед каждым текстовым блоком (я уже показал, что он есть перед первым и вторым), поэтому, когда мы станем работать с полным скриптом игры, нам придётся вынимать и вставлять обратно этот байт атрибута неупакованным (небольшое затруднение, с которым Круптар с честью справится). Даже, несмотря на то, что указатель-то ссылается как раз на текстовый блок за байтом атрибута, а не на сам атрибут:

 

$A645:B9 3E AA  LDA $AA3E,Y @ $AA40 = #$B4; Загрузка младшего байта указателя на второй текстовый блок. (в $AA3E таблица указателей)

$A648:38         SEC                       

$A649:E9 01      SBC #$01                        ;Отнимаем единичку! Теперь будет загружаться байт, стоящий перед вторым текстовым блоком                  

$A64B:85 10      STA $0010 = #$00          

$A64D:B9 3F AA  LDA $AA3F,Y @ $AA41 = #$AA; Загрузка старшего байта указателя на второй текстовый блок

$A650:E9 00      SBC #$00                  

$A652:85 11      STA $0011 = #$A2          

$A654:A0 00     LDY #$00                  

$A656:B1 10      LDA ($10),Y @ $AAB3 = #$03 ;Непосредственно загрузка байта атрибута.

 

 

 Великолепно! Мы узнали всё, что нам нужно и ассемблерная часть на этом закончена.

Что теперь? Ну, в былые времена, я бы посоветовал вам прикрутить к тестовой программке поддержку таблиц (что само по себе уже нетривиальная задача), затем поддержку использования и изменения указателей, сохранять всё это в текстовый файл и молиться каждый раз как вставляете текст, чтобы получившийся упакованный скрипт не налез на какие-нибудь жизненно важные для игры данные или делать проверку на размер получившегося выходного потока, что также не прибавляет программе легковесности.

Но мы живём в новой эре, дружище. Поэтому для всех этих рутинных процедур можно использовать Круптар, а получившийся проект будет являться компактным файликом, который содержит скрипт правильно отформатированный по переносам строк, с возможностью поиска и тому подобными милыми вещами. Более того, поддержка пойнтеров добавится почти автоматически! А работать переводчику в таком проекте просто одно удовольствие: не укладываешься в рамки отведённого пространства  – Круптар деликатно напомнит об этом. В общем, ромхакер делает то, что ему действительно нравится – остаётся один на один только с процедурой упаковки/распаковки данных – все побочные телодвижения (типа вынимания скрипта,  работы с таблицами и пойнтерами) производятся в Круптаре за пару минут.

Новая эра.

Итак, приступим к делу: наша задача написать плагин к Круптару, который распаковывал бы входной поток, представлял всё это в понятном для Круптаре виде, а затем, после перевода делал бы всё наоборот.

Несколько слов о плагинах: на самом деле исходный текст плагина – это простой проект в Дельфи с расширением .dpr и после компиляции будет представлять собой динамическую библиотеку, вроде тех, что использует система (.dll), только с расширением .kpl. С нашими исходными файлами можно работать как с простым проектом Дельфи, то есть существует возможность запускать приложение для отладки (только для этого необходимо указать Круптар, как Host приложение (Run ->Parameters… ->Local ->Host Application)). Компилировать же плагин можно и без Круптара. Стоит также отметить, что Круптар всегда использует плагины. И даже, когда мы вытаскиваем неупакованный скрипт, в Круптар загружен плагин «Standard.kpl».

Писать свой плагин, разумеется, мы будем не с чистого листа. Печка, от которой будем плясать – «Null», то есть пустой плагин (плагин для извлечения строк стандартного формата PChar - то есть с ноликом в конце строки).  Итак, откроем Null.dpr и внимательно изучим. Сразу переименуем его (мой называется SR.dpr) Попутно можно будет поменять имя и описание плагина (KPLDescription) с Null на что-нибудь более содержательное (этот текст будет отображаться в окне Справка -> Библиотеки). Пока что нас интересует только распаковка (о запаковке можно позаботиться потом – пока что просто достанем наш скрипт в читаемом виде).

Распаковка.

После подключения к проекту Круптара нашего плагина весь входной поток скрипта будет проходить через функцию «GetStrings», за один проход которой будет обработано одно сообщение (или в общем случае, блок, на который ссылается указатель, введённый в проект Круптара):

 

Function GetStrings(X, Sz: Integer): PTextStrings; stdcall; // X - Смещение текстовых данных в роме

                                                              // Sz - размер строки, указанный в ptStringLength (его можно

                                                              //не использовать в своих плагинах)

Var P: PChar; // null-terminated string

begin

 Result := NIL;

 If (X >= RomSize) or (X < 0) then Exit; // RomSize – размер загруженного в проект РОМа

 New(Result, Init); //Интциализация

 With Result^.Add^ do

 begin

  P := Addr(ROM^[X]); // ROM: Pbytes – типизированный указатель на данные РОМа

  Str := '';

  While P^ <> #0 do //Если встретили ноль, значит строка закончена

  begin

   Str := Str + P^; //Копируем данные в строку

   Inc(P);

  end;

 end;

end;

 

Тут ничего не скажешь: Djinn в первую очередь заботился об эффективности и быстродействии, а не о наглядности кода. Хотя, вся нужная нам информация может быть найдена в исходниках плагина (в т.ч. см. Needs.pas), всё же поясним тип возвращаемого функцией результата – PtextStrings:

PTextStrings = ^TTextStrings; // PTextStrings - это динамический массив состоящий из PTextString

 TTextStrings = Object

  Root, Cur: PTextString;// Root - указатель на первый элемент массива

  Count: Integer; // количество элементов в массиве

  Constructor Init; //подробно см. Needs.pas

  Function Add: PTextString;

  Function Get(I: Integer): PTextString;

  Destructor Done;

 end;

 

где PTextString:

PTextString = ^TTextString;

 TTextString = Record

  Str: String;

  Next: PTextString;

 end;

 

В функции GetStrings и будет происходить наша распаковка. Вот один из вариантов реализации этого (код учебный, поэтому в нём даже есть лишние переменные, добавленные для наглядности):

 

Function GetStrings(X, Sz: Integer): PTextStrings; stdcall;

Var B,SI,DI,DB,Bit: byte; //Переменные, как и в моей тестовой программе

begin

 Result := NIL;

 Bit:=0;

 SI:=0;

 DI:=0;

 DB :=0;

 If (X >= RomSize) or (X < 0) then Exit;

 New(Result, Init);

 With Result^.Add^ do

  begin                 //Легко узнать в дальнейших строках мою тестовую программу

  Str:='';

  Str := Str + Char(ROM^[X]); // первый байт не распаковываем - атрибут.

  inc (x);

  B := ROM^[X];

 

  Repeat  //бесконечный цикл, выход из которого возможен только по Break

   Bit := (B and $80) shr 7; //берём старший бит входного байта

   B := B shl 1;

   inc (SI);

   If SI = 8 Then

   Begin //Если байт входного потока исчерпан, читаем новый

    SI :=0;

    inc(X);

    B := ROM^[X];

   end;

 

   DB := DB shl 1;

   DB:= DB or Bit; //Записываем наш бит в младший бит выходного потока

   inc (DI);

   If DI = 6 then

   Begin //Если байт выходного потока исчерпан, сохраняем его в выходной поток.

    DI:=0;

    if DB = $3F then

    begin //Если распакован символ конца сообщения, сохраняем его в выходной поток и выходим.

     Str := Str + Char(DB);

     break ;

    end

    else

    Str := Str + Char(DB);

    DB :=0;

   end;

 

  Until False;

 end;

end;

 

Компилируем наш .dpr c изменённой функцией GetStrings и получаем файл с расширением .kpl, который нужно поместить в папку \Lib, расположенную в корневом каталоге Круптара.

Проект.

Пришло время создать непосредственно проект, с которым сможет работать переводчик. Открываем Круптар, жмём Ctrl+N  и в первую очередь вводим имена оригинального и переведённого РОМов (разумеется, пока что это два одинаковых файла). В моём случае это соответственно ‘Snake's Revenge (U).nes’ и ‘Snake's Revenge (Rus).nes’ (на случай, если вы будете открывать мой проект). Далее жмём Ctrl+T и добавляем таблицу из файла (или сразу жмём Ctrl+Alt+T), хотя можно составить таблицу прямо в проекте. Убедитесь, что Круптар правильно воспринял байты окончания и разрыва строк и не выдал предупреждения по загруженной таблице. Теперь жмём Ctrl+G и добавляем группу. Тут же указываем в графе grLibrary наш плагин из выпадающего списка (мой называется SR.kpl). grIsDictionary оставляем false. Далее добавляем блоки для текста – это место в РОМе, которое Круптар будет использовать для вставки обратно переведённого скрипта. Ну, начало-то мы знаем где: исходя из значения первого указателя в таблице и учтя, что перед первым текстовым блоком есть ещё один байт атрибута, начало будет по смещению 0хEA88. А конец где? Пока что это не так важно – пока что мы и не вставляем-то толком скрипт. Укажем это место наобум и можно с запасом: 0xFFFF. Затем выбираем пойнтеры, и «добавить элемент». Здесь опций уже больше.

 В итоге у вас должно получиться что-то вроде этого:

 

Здесь я менял только поля используемых таблиц, ptPointeSize и ptReference. Далее я расскажу, что означает каждая графа, а те, кому не интересны все возможности Круптара, могут смело переходить к чтению последнего абзаца этой части.

 

- ptInput/OutputTable – это таблицы, по которым будет производиться, соответственно, вынимание и вставка скрипта (в моём случае, эти таблицы одинаковые, так как я еще не перерисовывал шрифт и не знаю кодов будущих русских букв).

- ptDirctionary указывается группа-словарь, это применяется при работе DTE/MTE скриптами. Группа становится словарём, если grIsDictionary = TRUE, и становятся активны опции:

Параметры словаря;

Генерировать словарь;

Словарь в таблицу;

Опция "Генерировать словарь" берёт все списки, в которых указан словарь в ptDictionary и генерирует по тексту словарь, все слова заносятся в эту группу по порядку. Но работает эта опция не очень эффективно.

Борьба с DTE/MTE при помощи Круптара выходит за рамки этого документа, но те, кто заинтересовался этой темой, могут ознакомиться с проектом под Beetlejuice на NES, где для основного скрипта применяется MTE.

- ptPointerSize – размер указателей в байтах (на NES двухбайтовые указатели). Здесь может быть и ноль. Например, в некоторых играх указателей на отдельные строки нет вообще (какие адреса писать при добавлении  блока пойнтеров см. объяснение ptStringLength далее).

- ptReference – очень важный параметр, связанный с особенностью работы Круптара. Объясню подробнее: как только вы укажете Круптару смещение указателя, он считает его в соответствии с заданным форматом (ptPointerSize, ptMotorola и т.п.), затем прибавит к значению данного указателя величину ptReference и получит смещение первого байта входного текстового блока. Как видите, в моём случае, это h400F. Как уже много раз было сказано, первый байт упакованного текстового блока расположен по смещению 0хEA89, а значение первого указателя: $AA79 (первый указатель расположен по смещению 0xEA4E). Таким образом, hEA89-hAA79 = h4010. Не забудем про байт атрибута: ведь указатель ссылается на текстовый блок, а не на него: придётся отнять ещё единичку, чтобы наш атрибут вошёл в скрипт. Итого получаем h400F. Проверяем: если мы скормим Круптару указатель по смещению 0xEA4E, он прочитает его как величину  $AA79, затем прибавит к ней полученное только что нами h400F и начнёт читать из РОМа байт по смещению 0хEA88 – то, что надо, это же наш первый байт атрибута ($03)!

- ptInterval – промежуток в байтах между отдельными указателями (у нас указатели хранятся в монолитном едином блоке).

- ptShiftLeft - [исходный пойнтер] shl [значение в этом поле]. Часто формат указателей в игре такой, что для их использования приходится производить сдвиг байта влево, причем иногда не один раз.

- ptMultiply – очень часто при вставке скрипта обратно необходимо, чтобы начало строк было кратно по адресу какому-либо числу, которое и указывается в этом поле (например, на GBA некоторые инструкции правильно считывают данные только кратные четырём: ldr r0,[r1] – здесь адрес в r1 должен быть кратен 4 обязательно). Это связано с особенностями железа. И если предположить, что у нас GBA проект и наша строка может начаться, скажем, с адреса: 0х1АB4A1, то Круптар забивает 1AB4A1,1AB4A2,1AB4A3 нулями, а потом вставляет наш текст в 1AB4A4, соответственно корректируя указатели на эту строку.

- ptStringLength – указывается длина в байтах каждой строки. Фиксированная длина строки встречается во многих играх. Если ptPointerSize равен нулю, а ptStringLength не равен нулю, то при добавлении блока пойнтеров нужно вписывать адреса первой и последней строк. Если же ptPointerSize и ptStringLength будут равны нулю, то вписываем адреса первого и последнего байтов текстового блока (внутри блока Круптар будет ориентироваться по знаку окончания строки). И, как обычно, если ptPointerSize не равен нулю, то вписываем адреса первого и последнего указателей.

- ptMotorola – способ хранения указателей: в случае NES младший байт указателя хранится перед старшим, в процессорах Motorola – наоборот.

- ptSigned – указатели, которые могут иметь положительное и отрицательное значение (т.е. адресуют вперёд и назад от своей позиции) иногда применяются в играх на SEGA. Как правило, в РОМе стоит блок с текстом, а после него пойнтер, указывающий назад на нужную строку (скажем, имеем указатель со значением «-34»; Круптар берет адрес пойнтера, потом читает его значение, отсчитывает 34 байта назад и считывает эту строку). Если указатели двухбайтные, то они могут принимать значения от -32768 до +32767. Указатели, имеющие положительные значение, естественно, адресуют вперёд. Ещё одна особенность ptSigned: ptReference в этом случае уже можно не вписывать, так как адресация будет происходить от смещения в РОМе указателя.

ptPunType - такой тип используется в игре The Punisher (NES),  от того и название "PunType". А смысл здесь в том, что указатель сам по себе разделён некоторым количеством байт, которое в свою очередь указывается в ptInterval. Скажем, это мне очень пригодилось при переводе BEE-52, где почти все указатели хранятся в операндах ассемблерных команд:

LDX #$B4; младший байт указателя

LDY #$82; старший байт указателя

….

Если кто не знаком с ассемблером 6502, то  в хексредакторе мы этот код увидим в виде:

$A2 $B4 $A0 $82 – как видим, между старшим и младшим байтами указателя стоит не интересующий нас байт. Вот тут мы выставляем в поле ptPunType true, а в графе ptInterval пишем 2. Почему не 1? Это дань совместимости проекта с предыдущими версиями Круптара. В случае если ptPunType = true, в графу ptInterval вписывается «значение = количество_байт_в_разрыве +1». Подчеркну, что это не распространяется на случай, когда у нас есть интервал между самими указателями (prPunType = false).

- ptAutoStart – если значение этого поля true, то значение поля ptReference становится равным смещению в РОМе первого указателя в таблице (ptReference уже не заполняем), и к значению каждого указателя в этой таблице прибавляется ptReference, чтобы получить адрес строки (значение каждого указателя в этом случае есть смещение от начала таблицы до первого байта строки, на которую ссылается этот указатель).

- ptPtrToPtr – эта опция применяется в случае, если указатель ссылается на ещё один указатель, который в свою очередь ссылается на строку текста.

 

Отлично, продолжаем разбираться с нашим проектом: теперь жмём «добавить блок пойнтеров» и вводим смещения начала и конца таблицы указателей (если ещё кто не понял,  границы таблицы легко определяются визуально):

hEA4EhEA86 (мы указываем адреса первых байт указателя). Жмём ОК и, если вы всё делали правильно, случится чудо! Упакованный скрипт предстанет перед вами во всей красе (кстати, вместе с байтом атрибута). Если нет – сравни файл проекта с моим – может быть, что-нибудь упустил. Если и тут всё верно – как пить дать, намудрил с плагином. Полюбовавшись скриптом и отметив, что все буквы стоят на своих местах и везде имеются байты разрыва и окончания строки, будем двигаться дальше.

Упаковка.

С тем плагином, что мы написали, пытаться вставить скрипт обратно не имеет никакого смысла – игра его не воспримет (может быть, даже не влезет в отведенное место, которое мы и так взяли с запасом).  После подключения к проекту Круптара нашего плагина весь выходной поток, возвращаемый в РОМ, будет проходить через функцию «GetData», за один проход которой будет обработано одно сообщение (или в общем случае, блок, на который ссылается указатель, введённый в проект Круптара):

 

Function GetData(TextStrings: PTextStrings): String; stdcall; //Все типы переменных описаны при разборе GetString

Var R: PTextString;// Методы этого класса (см. Needs.pas):

                    // Add - добавляет новый элемент

                    // Root - возвращает пойнтер на первый элемент

                    // Cur – возвращает пойнтер на текущий добавленный - то есть на последний

begin

 Result := '';

 If TextStrings = NIL then Exit; //Если строк нет, выходим

 With TextStrings^ do

 begin

  R := Root; // R - первый элемент массива

  While R <> NIL do

  begin

   Result := Result + R^.Str + #0; //Возвращаем null-terminted string

   R := R^.Next; // R= следующий элемент массива

  end;

 end;

end;

 

Думаю, пояснять что-либо излишне: по уже сложившемуся алгоритму модифицируем эту функцию следующим образом:

 

Function GetData(TextStrings: PTextStrings): String; stdcall;

Var

R: PTextString;

I: integer; // Счётчик прочитанных букв для контроля не вышли ли мы за границы сообщения.

Bit, DB, DI, SB, SI: byte; // Полная аналогия с предыдущими функциями

begin

 Result := '';

 DB:=0;

 SB:=0;

 DI:=0;

 SI := 0;

 Bit:=0;

 I := 1;

 

 If TextStrings = NIL then Exit;

 With TextStrings^ do

 begin

   R := Root;

   With R^ do

   Result := Result + Char(Str[I]);   //1-ый байт не упаковываем - атрибут.

 While R <> NIL do

  begin

//-------------------------Моя процедура--------------------

 

With R^ do begin

 

inc(I);

SB := Byte(Str[I]);

SB := SB shl 2;             //Старшие два бита кода буквы не используются.

 

Repeat                     //бесконечный цикл, выход из которого возможен только по Break.

 Bit := (SB and $80) shr 7; //берём старший бит кода буквы.

 SB := SB shl 1;

 inc (SI);

 If SI = 6 then

 begin                     //Кончился текущий байт.

  inc (I);

  if I > Length(Str) then

  Begin                                   //Кончился весь входной поток.

   DB := DB or Bit;                       //(закидываем в выходной (упакованный) поток последний бит

   Result := Result + Char(DB shl (7-DI));//и добиваем нулями последний байт выходного потока).

   break;

  end;

  SB := Byte(Str[I]);                        //Читаем код очередной буквы из скрипта Круптара.

  SB := SB shl 2;                          //Старшие два бита кода буквы не используются.

  SI:=0;

 end;

 

 DB := DB or Bit;                      //Записываем наш бит в младший бит выходного потока.

 If DI = 7 then

 begin                                 //Если байт выходного потока записан полностью, сохраняем его.

  DI :=0;

  Result := Result + Char(DB);

  DB := 0;

 end

 else

 begin                                //Если записан не полностю, продолжаем процедуру упаковки.

  DB := DB shl 1;

  inc (DI);

 end;

 

until false

end;      //для with

 

//------------------------Конец моей процедуры----------------

   R := R^.Next;

  end;

 end;

end;

 

Получилось несколько больше, чем функция распаковки, зато теперь мы сможем смело вставлять скрипт назад. Компилируем получившийся плагин (уже с двумя модифицированными функциями) и заменяем им тот, который был раньше в папке \Lib (имя плагину менять не стоит – в проекте Круптара уже прописано старое, а прописать новый плагин возможно только, если все группы проект пусты). Снова открываем наш проект в Круптаре и на этот раз смело жмём Ctrl+F9, пока не изменяя содержимого скрипта.  

Как проверить – всё ли правильно мы упаковали? Теоретически, оригинальный РОМ и тот, куда мы только что вставили скрипт, должны быть абсолютно одинаковыми.  Нам нужно будет сравнить по содержимому эти два файла. Я пользуюсь Сравнением по Содержимому в Total Commander: выделяем оба РОМа и жмём Shift+F1. Что за чёрт?! Почему появились различия? Без паники. В первую очередь, становится ясно, что различия есть и в таблицах указателей и во вставленных текстовых блоках, но различия эти явно сформированы в блоки – это видно невооружённым глазом. А произошло следующее. В оригинальной игре структура указателей и текстовых блоков была схематично такой:

 

 

В этом легко убедиться: величины указателей в таблице не всегда возрастают (оставим топорную работу с текстом на совести разработчиков). Круптар вынимает скрипт из РОМа, естественно, по указателям: текстовые блоки в проекте идут по порядку появления указателей в таблице (то есть с точки зрения проекта Круптара сначала идёт 1, потом 3, 4 и 2 текстовые блоки), а вот вставляет он скрипт обратно, ориентируясь уже только на текстовые блоки (что логично, ведь после перевода они могли измениться в размерах), а уже потом корректирует указатели под полученную структуру блоков:

 

 

Вот что мы имеем в выходном файле, но с точки зрения игры ничего не изменилось: скажем, номер сообщения равен двум: как в оригинальном случае, так и после обработки скрипта Круптаром, игра загружает второй по счету указатель (на третий текстовый блок) и начинает читать из третьего текстового блока. Поэтому мы не только не навредили, но даже привели в порядок блоки сообщений в игре! Все эти теоретические догадки подтверждаются элементарным тестированием. Вот первый самовольно перемещённый Круптаром текстовый блок (в скрипте он шестой по счёту):

 

Всё работает правильно и появляется в нужном месте. Как видите, я даже немного поменял текст, чтобы убедиться в правильности упаковки.

Остался у нас один должок – это я про то, что мы не знали где конец последнего упакованного блока, а значит, не могли точно определить границы блоков для текста, помнишь? Самое время сделать это. Дело в том, что Круптар после упаковки текста в указанное нами место («блоки для текста») заполняет оставшиеся до конца зарезервированного блока байты нулями. Соответственно, так как пакует наш плагин точно так же, как и игра, начало большого блока с нулями в изменённом РОМе будет свидетельствовать о конце упакованного текста. В инструменте сравнения по содержимому нужно отыскать нули в изменённом РОМе и посмотреть, где они начинаются: в нашем случае это смещение 0xF0DC (кстати, сразу после него начинается блок с какими-то указателями). В проекте вбиваем этот адрес как конечный (не забываем восстановить переводимый РОМ – он уже запорот), и получаем абсолютно корректный скрипт, с которым можно спокойно работать переводчику.

Конечно, с непривычки может показаться, что всё это дело очень сложное. Однако поверьте мне на слово: без всего этого можно провозиться раз в пять дольше. Помнится, когда я писал всё по старинке – распаковщик вышел через три дня, а упаковщик – ещё дня через два (я, правда, до сих пор не могу понять, как он работал). На написание всего этого документа, вместе с изучением алгоритма и вознёй с картинками у меня ушёл ровно один день.

The End